[JAVA] Obtenez des informations vidéo de Nikorepo et envoyez-les à Slack

Ceci est l'article sur le 24e jour du Calendrier de l'Avent Nandemo Ali 2018. Cela fait déjà 25 jours, mais je pense qu'un jour est une erreur. Le code lui-même peut être trouvé sur github.

introduction

C'est l'histoire d'un bot qui publie automatiquement l'URL de la vidéo sur Slack depuis Nico Repo de Nico Nico Video. En fait, ce n'est pas encore terminé, mais après cela, il est bon que ce ne soit pas le point principal, comme l'exécuter régulièrement et faire face à des entrées inattendues. Je ferai le reste petit à petit après le mémoire de maîtrise.

Chose que tu veux faire

Nikorepo, si vous ne suivez pas l'affiche sur Twitter, il est essentiel de trouver de nouvelles vidéos dans votre série préférée. Pour être honnête, il y a beaucoup de bruit et c'est ennuyeux. ニコレポ Je veux juste des informations sur les nouvelles vidéos, mais en fonction de la vidéo, j'ai rempli le Nikorepo avec "Publié", "Enregistré dans ma liste", "Nikoni a annoncé", "n joué", "classé à la nième place" Je ferai de mon mieux. Il existe une fonction de mise en sourdine, mais comme il s'agit d'une fonction de mise en sourdine d'une notification spécifique d'un utilisateur spécifique, elle ne peut pas être supprimée car vous souhaitez supprimer le contenu que vous souhaitez supprimer.

En ce qui concerne l'ambiance, une fois que les informations sur la vidéo apparaissent, je souhaite exclure la même vidéo de Nikorepo pendant un moment. Je veux donc créer un bot qui n'obtienne que les informations vidéo de Nikorepo, supprime les doublons, puis les envoie à Slack.

C'est difficile à comprendre, mais comme ça, ça devient un bot qui met l'URL de la vidéo sans duplication. 結果

la mise en oeuvre

Pour être honnête, je n'écris rien parce que j'ai des cheveux sur Hello World.

HttpClient En fait, ce que je voulais faire était un demi-bonus, et le but principal était d'essayer d'utiliser les classes autour de HttpClient ajoutées dans Java 11. Lors de l'envoi de HttpRequest avec Java, c'était comme pétrir HttpURLConnection ou utiliser une bibliothèque telle qu'Apache Commons jusqu'à il y a quelque temps, mais HttpClient a été ajouté à partir de Java 11.

Je l'ai légèrement omis, mais ce n'est pas grave si vous jetez simplement la valeur d'en-tête dans le générateur, que vous la construisez et que vous l'envoyez. C'est très facile à comprendre par rapport à quelque chose comme HttpURLConnection qui s'ouvre et lance. Si vous utilisez Java 11, je pense que vous pouvez écrire sans problème sans utiliser de bibliothèque.

HttpClient.java


	private Map<String, String> headers;
	private HttpClient httpClient;

	public HttpResponse<String> sendPost(String messages) throws IOException, InterruptedException {
		Builder builder = HttpRequest.newBuilder(URI.create(uri));
		headers.entrySet().stream()
				.forEach(entry -> builder.header(entry.getKey(), entry.getValue()));
		builder.POST(BodyPublishers.ofString(messages));
		return httpClient.send(builder.build(), BodyHandlers.ofString());
	}

Slack RTM API

Connectez simplement le WebSocket au wss renvoyé en envoyant un message à l'URL de connexion préparée par Slack. Il semble que la connexion sera coupée si vous n'envoyez pas régulièrement de ping, donc pour le moment, le ping est lancé toutes les secondes.

SlackClient.java


public class SlackClient{
	
	private ConnectionInfo connectionInfo;
	private SlackListener listener = new SlackListener();
	private ResponseProcessor processor = new ResponseProcessor();
	private SlackSpeaker speaker;
	private String token;

	public SlackClient(String token) {
		this.token = token;
	}

	public static void main(String[] args) {
		if(args.length != 1)
			return;
		SlackClient client = new SlackClient(args[0]);
		try {
			client.start();
		} catch (IOException | InterruptedException | ExecutionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	private boolean connect() throws IOException, InterruptedException {
		Map<String, String> headers =  Map.of("Content-type", "application/x-www-form-urlencoded");
		Map<String, String> postMessages = Map.of("token", token);
		SimpleHttpClient httpClient = new SimpleHttpClient(SlackAPI.CONNECT.getURIText(), headers, postMessages);
		
		HttpResponse<String> response = httpClient.sendPost();
		
		Gson gson = new Gson();
		ConnectionInfo connectionInfo = 
				gson.fromJson(response.body(), ConnectionInfo.class);
		this.connectionInfo = connectionInfo;
		System.out.println(gson.toJson(connectionInfo));
		
		return connectionInfo.isSucceed();
	}
	
	private WebSocket createWebSocket() throws InterruptedException, ExecutionException {
		HttpClient client = HttpClient.newHttpClient();
		CompletableFuture<WebSocket> future = client
				.newWebSocketBuilder()
				.buildAsync(URI.create(connectionInfo.getURI()), listener);
		return future.get();
	}
	
	public void start() throws IOException, InterruptedException, ExecutionException {
		connect();
		
		speaker = new SlackSpeaker(createWebSocket());
		listener.subscribe(processor);
		processor.subscribe(speaker);
		
		while(true) {
			speaker.sendPing();
			Thread.sleep(1000);;
		}
	}
}

La réception des messages de Slack, le traitement du contenu des messages et l'envoi de messages sont implémentés séparément parce que je voulais séparer les classes.

Pour l'implémentation de cette zone, java.util.concurrent.Flow introduit à partir de Java 9 est utilisé. Publisher, Processor et Subscriber ont des sections que je ne comprends et n'utilise honnêtement pas dans une atmosphère, mais même si elles sont implémentées grossièrement, elles peuvent être traitées de manière asynchrone dans un modèle éditeur-abonné, ce qui est pratique.

L'interface d'écoute de WebSocket avait la même atmosphère, donc c'était facile à attacher.

SlackListener.java


public class SlackListener extends SubmissionPublisher<String> implements Listener{
	private List<CharSequence> messageParts = new ArrayList<>();
	private CompletableFuture<?> accumulatedMessage = new CompletableFuture<>();
	
	@Override
	public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last){
		messageParts.add(data);
		webSocket.request(1);
		if(last) {
			submit(String.join("", messageParts));
			messageParts = new ArrayList<>();
			accumulatedMessage.complete(null);
			CompletionStage<?> cf = accumulatedMessage;
			accumulatedMessage = new CompletableFuture<>();
			return cf;
		}
		return accumulatedMessage;
	}
	
	@Override
	public void onError(WebSocket webSocket, Throwable error) {
		error.printStackTrace();
	}
}

Classe de traitement de messages avec le sentiment que je voudrais ajouter à l'avenir diverses fonctions autres que la fonction de lancement Nikorepo. La chaîne publiée par bot est également acquise ici.

Vous pouvez également obtenir la chaîne en la lançant dans conversations.list. Cette fois, le Json du message envoyé depuis slack incluait normalement channelId, donc c'était "Est-il plus facile de tirer à partir de là?", Donc le bot publie sur le canal où l'utilisateur a entré une commande spécifique. Mis en œuvre en tant que.

J'ai décidé de transmettre l'adresse e-mail et le mot de passe nécessaires pour me connecter à Nico Nico via le message de Slack. Celui-ci semblait être plus facile lors de la connexion d'un composé ou d'une autre personne qui doit se connecter.

ResponseProcessor.java


public class ResponseProcessor extends SubmissionPublisher<TransmissionMessage> 
		implements Processor<String, TransmissionMessage> {
	private Subscription subscription;
	private List<String> activeChannels = Collections.synchronizedList(new ArrayList<>());
	private NiconicoClient niconicoClient = new NiconicoClient(this::sendMessage);
	private Gson gson = new Gson();
	
	@Override
	public void onSubscribe(Subscription subscription) {
		this.subscription = subscription;
		subscription.request(1);
	}

	@Override
	public void onNext(String message) {
		System.out.println("onMessage : " + message);
		MessageType type = convertType(message);
		
		switch(type) {
		case MESSAGE:
			processMessage(message);
			break;
		case LOG:
			processLog(message);
			break;
		}
		subscription.request(1);
	}
	
	private void sendMessage(String message) {
		activeChannels.parallelStream()
				.map(channel -> new TalkMessage(message, channel))
				.forEach(this::submit);
	}
	
	private void processLog(String message) {
		LogMessage log = gson.fromJson(message, LogMessage.class);
		if(log.isOk())
			return;
		System.err.println(log.getError());
	}
	
	private void processMessage(String message) {
		ResponseMessage response = gson.fromJson(message, ResponseMessage.class);
		String text = response.getText();
		if(text != null && text.startsWith("command:")) {
			processCommand(text.split("(^command): *")[1], response.getChannel());
			return;
		}
	}
	
	private void processCommand(String command, String channel) {
		String[] array = command.split(" ");
		switch(array[0]) {
		case "activate":
			switch(array[1]) {
			case "bot":
				activeChannels.add(channel);
				sendMessage("Je publierai sur cette chaîne.");
				break;
			case "nicorepo":
				if(activeChannels.isEmpty())
					break;
				try {
					String email = array[2].split("\\|")[0].split(":")[1];
					niconicoClient.login(email, array[3]);
					niconicoClient.getMyRepoData();
				} catch (IOException | InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				break;
			}
			break;
		}
	}
	
	private MessageType convertType(String message) {
		try {
			InputStream inputStream = new ByteArrayInputStream(message.getBytes("utf-8"));
			JsonReader reader = new JsonReader(new InputStreamReader(inputStream,"utf-8"));
			reader.beginObject();
			while(reader.hasNext()) {
				switch(reader.nextName()) {
				case "type":
					return MessageType.toMessageType(reader.nextString());
				case "ok":
					return MessageType.LOG;
				}
				break;
			}
			inputStream.close();
			reader.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return MessageType.OTHER;
	}

	@Override
	public void onError(Throwable throwable) {
		// TODO Auto-generated method stub
		throwable.printStackTrace();
	}

	@Override
	public void onComplete() {
		// TODO Auto-generated method stub
		
	}
}

Il n'y a rien de spécial à dire sur la classe d'envoi de message, alors coupez-le.

API vidéo Nico Nico

Fondamentalement, la même chose que la connexion de Slack. Étant donné que les informations de session lors de la connexion sont automatiquement redirigées après le processus de connexion, il est nécessaire de les définir sur Redirect.NEVER et d'acquérir le cookie sans rediriger dans le processus de connexion. Mon point de terminaison de dépôt n'est pas sorti même si je googlé, j'ai donc ouvert ma page en ouvrant les outils de développement du navigateur [point de terminaison comme ça](http://www.nicovideo.jp/api/nicorepo/timeline/ J'ai recherché mon / tout? Client_app = pc_myrepo).

NiconicoClient.java


public class NiconicoClient {
	
	private LoginInfo loginInfo;
	private HttpCookie cookie;
	private Deque<NiconicoReport> reports = new ArrayDeque<>();
	private long latestReportId = 0;
	private Consumer<String> sendMessage;
	
	public NiconicoClient(Consumer<String> sendMessage) { 
		this.sendMessage = sendMessage;
	}
	
	public boolean login(String mail, String password) throws IOException, InterruptedException {
		loginInfo = new LoginInfo(mail, password);
		Map<String, String> headers = Map.of("Content-type", "application/x-www-form-urlencoded");
		Map<String, String> postMessages = Map.of("next_url", "",
				"mail", loginInfo.getMail(), 
				"password", loginInfo.getPassword());
		SimpleHttpClient httpClient = 
				new SimpleHttpClient(NiconicoAPI.LOGIN.getURIText(), Redirect.NEVER, headers, postMessages);
		HttpResponse<String> response = httpClient.sendPost();
		
		if(!httpClient.isPresentCookieHandler())
			return false;
		CookieStore store = ((CookieManager)httpClient.getCookieHandler()).getCookieStore();
		cookie = store.getCookies().stream()
				.filter(cookie -> cookie.getName().equals("user_session") &&
						!cookie.getValue().equals("deleted"))
				.findAny().orElse(null);
		loginInfo.setSession(cookie.toString());
		return loginInfo.isLogin();
	}
	
	public void getMyRepoData() throws IOException, InterruptedException {
		Map<String, String> headers = Map.of("Cookie", loginInfo.getCookie());
		SimpleHttpClient httpClient = 
				new SimpleHttpClient(NiconicoAPI.MYREPO.getURIText(), Redirect.ALWAYS, headers, Map.of());
		HttpResponse<String> response = httpClient.sendGet();

		MyRepoData data = new Gson().fromJson(response.body(), MyRepoData.class);
		List<NiconicoReport> list = data.getReports().stream()
				.filter(report -> report.getLongId() > latestReportId)
				.sorted(Comparator.comparingLong(NiconicoReport::getLongId))
				.collect(Collectors.toList());
		latestReportId = list.get(list.size()-1).getLongId();
		String watchUri = NiconicoAPI.WATCH_PAGE.getURIText();
		list.stream()
				.filter(report -> report.getVideo() != null)
				.map(report -> report.getVideo().getWatchId())
				.distinct()
				.forEach(id -> sendMessage.accept(watchUri + id));
		
	}
}

Si vous lancez l'URL après avoir vérifié si elle a déjà été publiée avec celle obtenue à partir de My Repo et avoir jeté l'URL, vous pouvez publier l'URL de la vidéo de My Repo sur Slack sans duplication comme la première image.

Là où la mise en œuvre n'est pas terminée

J'ai écrit la majeure partie du traitement pendant la nuit d'hier, mais il y a plusieurs omissions et la mise en œuvre n'est pas terminée.

Je ne l'ai qu'une seule fois maintenant, mais bien sûr, je veux l'exécuter régulièrement et le lancer automatiquement une fois qu'il est activé. Le reste est fondamentalement basé sur votre propre utilisation, donc la gestion des exceptions est appropriée (si vous lancez une commande légèrement étrange, elle s'arrêtera probablement). À l'avenir, j'aimerais pouvoir échanger des commandes en langage naturel.

Pour le moment, sauf pour le dernier, quand j'en ai envie, ça se termine par un choi choi, donc je veux commencer à travailler après le mémoire de master.

Point de dépendance personnel

L'API de Slack, en gros, si vous envoyez un message invalide, une erreur sera renvoyée correctement (l'ID de canal est différent), mais si le message à envoyer n'est pas conforme au format (même s'il n'est pas au format Json), même un message d'erreur Ça ne revient pas. J'ai mis en place pour me connecter, envoyé même des caractères vides et essayé de voir si l'erreur était renvoyée correctement, et je suis resté bloqué pendant un moment. Pour vérifier si vous pouvez communiquer correctement avec WebSocket, essayez avec wscat ou la configuration minimale.

J'ai obtenu les informations avec l'API de Niconico et les ai sorties avec System.out.println pour le moment pour vérifier si elles ont été prises correctement, mais pour une raison quelconque, le texte n'a pas été affiché. En fait, le contenu lui-même pourrait être obtenu normalement, donc si vous jetez toutes les longues phrases de merde dans Eclipse, l'affichage sera bogué. La console est mauvaise.

Recommended Posts

Obtenez des informations vidéo de Nikorepo et envoyez-les à Slack
Obtenez des informations vidéo YouTube avec Retrofit et conservez-les dans l'application Android.
J'ai essayé d'appeler une vidéo YouTube de DB avec haml et de l'afficher intégrée
Comment obtenir les informations les plus longues de Twitter à partir du 12/12/2016
[Kotlin] Obtenez le constructeur / la méthode Java de KFunction et appelez-le
Comment obtenir et ajouter des données depuis Firebase Firestore dans Ruby
[Java] Comment convertir du type String en type Path et obtenir le chemin
Obtenir TypeElement et TypeMirror à partir de la classe
[Java] Obtenir et gérer Json à partir d'une URL avec une API standard (javax.script)
N'oubliez pas de relâcher lorsque vous récupérez l'objet de S3!
[Kotlin] Trois façons d'obtenir un cours depuis KClass
Obtenir des informations sur l'appelant à partir de la trace de la pile (Java)
[Java] Obtenir des informations sur les balises à partir de fichiers musicaux
Publier sur Slack à partir de Play Framework 2.8 (Java)
[Java] Exemple de programme qui acquiert les valeurs maximum et minimum d'un tableau
Jusqu'à ce que vous démarriez le serveur Zabbix avec docker-compose et que vous obteniez des informations d'autres hôtes