[JAVA] Holen Sie sich Videoinformationen von Nikorepo und werfen Sie sie zu Slack

Dies ist der Artikel am 24. Tag des Nandemo Ali Adventskalenders 2018. Es sind bereits 25 Tage, aber ich denke, ein Tag ist ein Fehler. Der Code selbst befindet sich unter github.

Einführung

Es ist die Geschichte eines Bots, der automatisch die Video-URL von Nico Repo von Nico Nico Video an Slack sendet. Eigentlich ist es noch nicht abgeschlossen, aber danach ist es gut, dass es nicht der Hauptpunkt ist, wie es regelmäßig auszuführen und mit unerwarteten Eingaben umzugehen. Den Rest werde ich nach und nach nach der Masterarbeit erledigen.

Was du machen willst

Nikorepo, wenn Sie dem Poster auf Twitter nicht folgen, ist es wichtig, neue Videos in Ihrer Lieblingsserie zu finden. Um ehrlich zu sein, gibt es viel Lärm und es ist nervig. ニコレポ Ich möchte nur Informationen zu neuen Videos, aber je nach Video habe ich die Nikorepo mit "Gepostet", "In meiner Liste registriert", "Nikoni beworben", "n gespielt", "auf dem n-ten Platz" ausgefüllt. Ich werde mein Bestes geben. Es gibt eine Stummschaltfunktion. Da es sich jedoch um eine Funktion zum Stummschalten einer bestimmten Benachrichtigung eines bestimmten Benutzers handelt, kann diese nicht gelöscht werden, da Sie den zu löschenden Inhalt löschen möchten.

Was die Stimmung betrifft, möchte ich, sobald die Videoinformationen angezeigt werden, dasselbe Video für eine Weile von Nikorepo ausschließen. Ich möchte also einen Bot erstellen, der nur Videoinformationen von Nikorepo erhält, Duplikate löscht und diese dann an Slack weiterleitet.

Es ist schwer zu verstehen, aber so wird es zu einem Bot, der die URL des Videos ohne Duplizierung setzt. 結果

Implementierung

Um ehrlich zu sein, schreibe ich nichts, weil ich Haare auf Hello World habe.

HttpClient Tatsächlich wollte ich einen halben Bonus machen, und der Hauptzweck bestand darin, die in Java 11 hinzugefügten Klassen um HttpClient zu verwenden. Beim Senden einer HttpRequest in Java war es früher so, als würde man HttpURLConnection kneten oder eine Bibliothek wie Apache Commons verwenden, aber Java 11 fügte HttpClient hinzu.

Ich habe es subtil weggelassen, aber es ist in Ordnung, wenn Sie nur den Header-Wert in den Builder werfen, ihn erstellen und senden. Es ist sehr leicht zu verstehen im Vergleich zu etwas wie HttpURLConnection, das sich öffnet und wirft. Wenn Sie Java 11 verwenden, können Sie meiner Meinung nach problemlos schreiben, ohne eine Bibliothek zu verwenden.

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

Verbinden Sie einfach das WebSocket mit dem zurückgegebenen wss, indem Sie einen Beitrag an die von Slack erstellte Verbindungs-URL senden. Es scheint, dass die Verbindung unterbrochen wird, wenn Sie nicht regelmäßig Ping senden. Daher wird Ping vorerst jede Sekunde ausgelöst.

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);;
		}
	}
}

Das Empfangen von Nachrichten aus Slack, das Verarbeiten von Nachrichteninhalten und das Senden von Nachrichten werden separat implementiert, da ich die Klassen trennen wollte.

Für die Implementierung dieses Bereichs wird der aus Java 9 eingeführte java.util.concurrent.Flow verwendet. Publisher, Processor und Subscriber haben Abschnitte, die ich ehrlich gesagt nicht verstehe und in einer Atmosphäre verwende, aber selbst wenn sie grob implementiert sind, können sie asynchron in einem Publisher-Subscriber-Muster verarbeitet werden, was praktisch ist.

Die Listener-Oberfläche von WebSocket hatte die gleiche Atmosphäre, so dass es einfach war, sie anzuhängen.

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();
	}
}

Nachrichtenverarbeitungsklasse mit dem Gefühl, dass ich in Zukunft verschiedene andere Funktionen als die Nikorepo-Wurffunktion hinzufügen möchte. Hier wird auch der von bot gepostete Kanal erfasst.

Sie können den Kanal auch abrufen, indem Sie ihn in die Konversationsliste werfen. Dieses Mal war die channelId normalerweise im Json der von slack gesendeten Nachricht enthalten, also war es "Ist es einfacher, von dort zu ziehen?". Der Bot postet also auf dem Channel, auf dem der Benutzer einen bestimmten Befehl eingegeben hat. Implementiert als.

Ich beschloss, die E-Mail-Adresse und das Passwort zu übergeben, die erforderlich sind, um mich über Slacks Nachricht bei Nico Nico anzumelden. Dieser schien einfacher zu sein, wenn Sie eine Verbindung oder eine andere Person anschließen, die sich anmelden muss.

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("Ich werde auf diesem Kanal posten.");
				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
		
	}
}

Die Klasse zum Senden von Nachrichten hat nichts Besonderes, also habe ich sie abgeschnitten.

Nico Nico Video API

Grundsätzlich das gleiche wie bei Slack's Connect. Da die Sitzungsinformationen beim Anmelden nach dem Anmeldevorgang automatisch umgeleitet werden und die Sitzungsinformationen beim Anmelden gelöscht werden, muss Redirect.NEVER im Anmeldevorgang festgelegt und das Cookie ohne Umleitung abgerufen werden. Mein Repo-Endpunkt wurde nicht angezeigt, selbst wenn ich gegoogelt habe. Daher habe ich meine Seite geöffnet, während ich die Entwicklertools des Browsers [so ein Endpunkt] geöffnet habe (http://www.nicovideo.jp/api/nicorepo/timeline/). Ich habe nach meinem / all? Client_app = pc_myrepo gesucht.

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));
		
	}
}

Wenn Sie die URL werfen, nachdem Sie überprüft haben, ob sie bereits mit der von My Repo erhaltenen URL gepostet wurde, und die URL werfen, können Sie die Video-URL von My Repo wie beim ersten Bild ohne Duplizierung an Slack senden.

Wo die Implementierung noch nicht abgeschlossen ist

Ich habe den größten Teil der Verarbeitung von gestern über Nacht geschrieben, aber es gibt verschiedene Auslassungen und die Implementierung ist noch nicht abgeschlossen.

Ich habe es jetzt nur einmal, aber natürlich möchte ich es regelmäßig ausführen und automatisch werfen, sobald es aktiviert ist. Der Rest basiert im Wesentlichen auf Ihrem eigenen Gebrauch, daher ist die Ausnahmebehandlung angemessen (wenn Sie einen etwas seltsamen Befehl auslösen, wird dieser wahrscheinlich gestoppt). In Zukunft möchte ich Befehle in natürlicher Sprache austauschen können.

Bis auf das letzte Mal, wenn ich Lust dazu habe, endet es vorerst mit einem Choi Choi, also möchte ich nach der Masterarbeit anfangen zu arbeiten.

Persönlicher Suchtpunkt

Slacks API: Wenn Sie eine ungültige Nachricht senden, wird im Grunde genommen ein Fehler ordnungsgemäß zurückgegeben (Kanal-ID ist unterschiedlich). Wenn die zu sendende Nachricht jedoch nicht dem Format entspricht (auch wenn sie nicht im Json-Format vorliegt), wird sogar eine Fehlermeldung angezeigt Es kommt nicht zurück. Ich habe bis zum Verbinden implementiert, sogar leere Zeichen gesendet und versucht, festzustellen, ob der Fehler ordnungsgemäß zurückgegeben wurde, und bin eine Weile hängen geblieben. Versuchen Sie es mit wscat oder der Mindestkonfiguration, um zu überprüfen, ob Sie ordnungsgemäß mit WebSocket kommunizieren können.

Ich habe die Informationen mit der API von Niconico erhalten und sie vorerst mit System.out.println ausgegeben, um zu überprüfen, ob sie richtig aufgenommen wurden, aber aus irgendeinem Grund wurde der Text nicht angezeigt. Tatsächlich könnte der Inhalt selbst normal abgerufen werden. Wenn Sie also alle verdammt langen Sätze in Eclipse werfen, ist das Display fehlerhaft. Die Konsole ist schlecht.

Recommended Posts

Holen Sie sich Videoinformationen von Nikorepo und werfen Sie sie zu Slack
Holen Sie sich Youtube-Videoinformationen mit Retrofit und behalten Sie sie in der Android-App.
Ich habe versucht, YouTube-Video von DB mit haml aufzurufen und es eingebettet anzuzeigen
So erhalten Sie die längsten Informationen von Twitter ab dem 12.12.2016
[Kotlin] Holen Sie sich Java Constructor / Method von KFunction und rufen Sie es auf
Abrufen und Hinzufügen von Daten aus dem Firebase Firestore in Ruby
[Java] So konvertieren Sie vom Typ String in den Pfadtyp und erhalten den Pfad
Holen Sie sich TypeElement und TypeMirror von Class
[Java] Json von der URL mit der Standard-API (javax.script) abrufen und verarbeiten
Vergessen Sie nicht freizugeben, wenn Sie das Objekt von S3 erhalten!
[Kotlin] Drei Möglichkeiten, um Klasse von KClass zu bekommen
Abrufen von Anruferinformationen aus dem Stack-Trace (Java)
[Java] Tag-Informationen aus Musikdateien abrufen
Post to Slack von Play Framework 2.8 (Java)
[Java] Beispiel eines Programms, das die Maximal- und Minimalwerte von einem Array abruft
Bis Sie Zabbix Server mit Docker-Compose starten und Informationen von anderen Hosts erhalten