プロジェクト

全般

プロフィール

« | » 

リビジョン 6a3fcb6d

みぞ @mizo0203 さんが5年以上前に追加

Add feature Mastodon's timeline talking. fix #422 @16.0h

差分を表示:

.idea/artifacts/TimelineTalker_jar.xml
2 2
  <artifact type="jar" name="TimelineTalker:jar">
3 3
    <output-path>$PROJECT_DIR$/out/artifacts/TimelineTalker_jar</output-path>
4 4
    <root id="archive" name="TimelineTalker.jar">
5
      <element id="module-output" name="TimelineTalker"/>
6
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/twitter4j/twitter4j-core/4.0.6/twitter4j-core-4.0.6.jar" path-in-jar="/" />
7
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/twitter4j/twitter4j-stream/4.0.6/twitter4j-stream-4.0.6.jar" path-in-jar="/" />
5
      <element id="module-output" name="TimelineTalker" />
6
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/google/code/gson/gson/2.8.0/gson-2.8.0.jar" path-in-jar="/" />
7
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/io/reactivex/rxjava2/rxjava/2.0.8/rxjava-2.0.8.jar" path-in-jar="/" />
8
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.2.0/kotlin-stdlib-1.2.0.jar" path-in-jar="/" />
9
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/github/sys1yagi/mastodon4j/mastodon4j/1.6.0/mastodon4j-1.6.0.jar" path-in-jar="/" />
10
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jre7/1.2.0/kotlin-stdlib-jre7-1.2.0.jar" path-in-jar="/" />
11
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" path-in-jar="/" />
12
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/github/sys1yagi/mastodon4j/mastodon4j-rx/1.6.0/mastodon4j-rx-1.6.0.jar" path-in-jar="/" />
13
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/squareup/okio/okio/1.11.0/okio-1.11.0.jar" path-in-jar="/" />
14
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/reactivestreams/reactive-streams/1.0.0/reactive-streams-1.0.0.jar" path-in-jar="/" />
15
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/twitter4j/twitter4j-core/4.0.7/twitter4j-core-4.0.7.jar" path-in-jar="/" />
16
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/commons-io/commons-io/2.6/commons-io-2.6.jar" path-in-jar="/" />
17
      <element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/squareup/okhttp3/okhttp/3.6.0/okhttp-3.6.0.jar" path-in-jar="/" />
8 18
    </root>
9 19
  </artifact>
10 20
</component>
TimelineTalker.iml
9 9
    </content>
10 10
    <orderEntry type="inheritedJdk" />
11 11
    <orderEntry type="sourceFolder" forTests="false" />
12
    <orderEntry type="library" name="Maven: org.twitter4j:twitter4j-core:4.0.6" level="project" />
13 12
    <orderEntry type="library" name="Maven: org.twitter4j:twitter4j-core:4.0.7" level="project" />
13
    <orderEntry type="library" name="Maven: com.github.sys1yagi.mastodon4j:mastodon4j:1.6.0" level="project" />
14
    <orderEntry type="library" name="Maven: org.jetbrains.kotlin:kotlin-stdlib-jre7:1.2.0" level="project" />
15
    <orderEntry type="library" name="Maven: org.jetbrains.kotlin:kotlin-stdlib:1.2.0" level="project" />
16
    <orderEntry type="library" name="Maven: org.jetbrains:annotations:13.0" level="project" />
17
    <orderEntry type="library" name="Maven: com.squareup.okhttp3:okhttp:3.6.0" level="project" />
18
    <orderEntry type="library" name="Maven: com.squareup.okio:okio:1.11.0" level="project" />
19
    <orderEntry type="library" name="Maven: com.google.code.gson:gson:2.8.0" level="project" />
20
    <orderEntry type="library" name="Maven: com.github.sys1yagi.mastodon4j:mastodon4j-rx:1.6.0" level="project" />
21
    <orderEntry type="library" name="Maven: io.reactivex.rxjava2:rxjava:2.0.8" level="project" />
22
    <orderEntry type="library" name="Maven: org.reactivestreams:reactive-streams:1.0.0" level="project" />
23
    <orderEntry type="library" name="Maven: commons-io:commons-io:2.6" level="project" />
14 24
  </component>
15 25
</module>
pom.xml
1
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3
	<modelVersion>4.0.0</modelVersion>
1
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
2
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3
    <modelVersion>4.0.0</modelVersion>
4 4

  
5
	<groupId>com.mizo0203</groupId>
6
	<artifactId>TimelineTalker</artifactId>
7
	<version>0.0.1-SNAPSHOT</version>
8
	<packaging>jar</packaging>
5
    <groupId>com.mizo0203</groupId>
6
    <artifactId>TimelineTalker</artifactId>
7
    <version>0.0.1-SNAPSHOT</version>
8
    <packaging>jar</packaging>
9 9

  
10
	<properties>
11
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
12
	</properties>
10
    <properties>
11
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
12
    </properties>
13 13

  
14
	<build>
15
		<sourceDirectory>src</sourceDirectory>
16
		<outputDirectory>target/classes</outputDirectory>
17
		<resources>
18
			<resource>
19
				<directory>src</directory>
20
				<excludes>
21
					<exclude>**/*.java</exclude>
22
				</excludes>
23
			</resource>
24
		</resources>
25
		<plugins>
26
			<plugin>
27
				<artifactId>maven-compiler-plugin</artifactId>
28
				<version>3.1</version>
29
				<configuration>
30
					<source>1.7</source>
31
					<target>1.7</target>
32
				</configuration>
33
			</plugin>
34
		</plugins>
35
	</build>
14
    <repositories>
15
        <repository>
16
            <id>jitpack.io</id>
17
            <url>https://jitpack.io</url>
18
        </repository>
19
    </repositories>
36 20

  
37
	<dependencies>
38
		<dependency>
39
			<groupId>org.twitter4j</groupId>
40
			<artifactId>twitter4j-core</artifactId>
41
			<version>[4.0,)</version>
42
		</dependency>
43
	</dependencies>
21
    <build>
22
        <sourceDirectory>src</sourceDirectory>
23
        <outputDirectory>target/classes</outputDirectory>
24
        <resources>
25
            <resource>
26
                <directory>src</directory>
27
                <excludes>
28
                    <exclude>**/*.java</exclude>
29
                </excludes>
30
            </resource>
31
        </resources>
32
        <plugins>
33
            <plugin>
34
                <artifactId>maven-compiler-plugin</artifactId>
35
                <version>3.1</version>
36
                <configuration>
37
                    <source>1.7</source>
38
                    <target>1.7</target>
39
                </configuration>
40
            </plugin>
41
        </plugins>
42
    </build>
43

  
44
    <dependencies>
45
        <dependency>
46
            <groupId>org.twitter4j</groupId>
47
            <artifactId>twitter4j-core</artifactId>
48
            <version>[4.0,)</version>
49
        </dependency>
50
        <dependency>
51
            <groupId>com.github.sys1yagi.mastodon4j</groupId>
52
            <artifactId>mastodon4j</artifactId>
53
            <version>[1.6,)</version>
54
        </dependency>
55
        <dependency>
56
            <groupId>com.github.sys1yagi.mastodon4j</groupId>
57
            <artifactId>mastodon4j-rx</artifactId>
58
            <version>[1.6,)</version>
59
        </dependency>
60

  
61
        <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
62
        <dependency>
63
            <groupId>commons-io</groupId>
64
            <artifactId>commons-io</artifactId>
65
            <version>2.6</version>
66
        </dependency>
67

  
68
    </dependencies>
44 69
</project>
src/META-INF/MANIFEST.MF
1 1
Manifest-Version: 1.0
2 2
Implementation-Title: TimelineTalker
3 3
Implementation-Version: 1.1
4
Main-Class: com.mizo0203.timeline.talker.Application
4
Main-Class: com.mizo0203.timeline.talker.Main
5 5

  
src/com/mizo0203/timeline/talker/Application.java
1
package com.mizo0203.timeline.talker;
2

  
3
import twitter4j.conf.Configuration;
4
import twitter4j.conf.ConfigurationBuilder;
5

  
6
/**
7
 * Java アプリケーション起動時に実行されるクラス
8
 * 
9
 * @author みぞ@CrazyBeatCoder
10
 */
11
public class Application {
12

  
13
  public static void main(String[] args) {
14
    TimelineTalker timelineTalker;
15
    Talker talker;
16

  
17
    try {
18
      talker = new Talker();
19
      timelineTalker =
20
              new TimelineTalker(new Arguments(args).twitterConfiguration, talker);
21
    } catch (IllegalArgumentException | IllegalStateException e) {
22
      System.err.println(e.getMessage());
23
      return;
24
    }
25

  
26
    timelineTalker.start();
27
    talker.talkAsync("アプリケーションを起動しました", Talker.YukkuriVoice.REIMU);
28
  }
29

  
30
  /**
31
   * Java アプリケーション起動時に指定する引数のデータクラス
32
   * 
33
   * @author みぞ@CrazyBeatCoder
34
   */
35
  private static class Arguments {
36

  
37
    private final Configuration twitterConfiguration;
38

  
39
    private Arguments(String[] args) throws IllegalArgumentException {
40
      if (args.length < Argument.values().length) {
41
        StringBuilder exceptionMessage = new StringBuilder();
42
        exceptionMessage.append(Argument.values().length + " つの引数を指定してください。\n");
43
        for (Argument arg : Argument.values()) {
44
          exceptionMessage.append((arg.ordinal() + 1) + " つ目: " + arg.detail + "\n");
45
        }
46
        throw new IllegalArgumentException(exceptionMessage.toString());
47
      }
48

  
49
      String consumer_key = args[Argument.CONSUMER_KEY.ordinal()];
50
      String consumer_secret = args[Argument.CONSUMER_SECRET.ordinal()];
51
      String access_token = args[Argument.ACCESS_TOKEN.ordinal()];
52
      String access_token_secret = args[Argument.ACCESS_TOKEN_SECRET.ordinal()];
53

  
54
      twitterConfiguration = new ConfigurationBuilder().setOAuthConsumerKey(consumer_key)
55
          .setOAuthConsumerSecret(consumer_secret).setOAuthAccessToken(access_token)
56
          .setOAuthAccessTokenSecret(access_token_secret).build();
57
    }
58

  
59
  }
60

  
61
  /**
62
   * Java アプリケーション起動時に指定する引数の定義
63
   * 
64
   * @author みぞ@CrazyBeatCoder
65
   */
66
  private enum Argument {
67
    CONSUMER_KEY("Twitter Application's Consumer Key (API Key)"), //
68
    CONSUMER_SECRET("Twitter Application's Consumer Secret (API Secret)"), //
69
    ACCESS_TOKEN("Twitter Account's Access Token"), //
70
    ACCESS_TOKEN_SECRET("Twitter Account's Access Token Secret"), //
71
    ;
72

  
73
    private final String detail;
74

  
75
    private Argument(String detail) {
76
      this.detail = detail;
77
    }
78
  }
79
}
src/com/mizo0203/timeline/talker/Arguments.java
1
package com.mizo0203.timeline.talker;
2

  
3
import com.google.gson.Gson;
4
import com.sys1yagi.mastodon4j.MastodonClient;
5
import okhttp3.OkHttpClient;
6
import org.jetbrains.annotations.NotNull;
7
import org.jetbrains.annotations.Nullable;
8
import twitter4j.conf.Configuration;
9
import twitter4j.conf.ConfigurationBuilder;
10

  
11
/**
12
 * Java アプリケーション起動時に指定する引数のデータクラス
13
 *
14
 * @author みぞ@CrazyBeatCoder
15
 */
16
/* package */ class Arguments {
17

  
18
  @Nullable /* package */ final Configuration twitterConfiguration;
19
  @Nullable /* package */ final MastodonClient mastodonClient;
20

  
21
  /* package */ Arguments(String[] args) throws IllegalArgumentException {
22
    if (args.length == Argument.values().length) {
23
      twitterConfiguration =
24
          createTwitterConfiguration(
25
              args[Argument.TWITTER_CONSUMER_KEY.ordinal()],
26
              args[Argument.TWITTER_CONSUMER_SECRET.ordinal()],
27
              args[Argument.TWITTER_ACCESS_TOKEN.ordinal()],
28
              args[Argument.TWITTER_ACCESS_TOKEN_SECRET.ordinal()]);
29
      mastodonClient =
30
          createMastodonClient(
31
              args[Argument.MASTODON_INSTANCE_NAME.ordinal()],
32
              args[Argument.MASTODON_ACCESS_TOKEN.ordinal()]);
33
    } else if (args.length == Argument.Twitter.values().length) {
34
      twitterConfiguration =
35
          createTwitterConfiguration(
36
              args[Argument.Twitter.CONSUMER_KEY.ordinal()],
37
              args[Argument.Twitter.CONSUMER_SECRET.ordinal()],
38
              args[Argument.Twitter.ACCESS_TOKEN.ordinal()],
39
              args[Argument.Twitter.ACCESS_TOKEN_SECRET.ordinal()]);
40
      mastodonClient = null;
41
    } else if (args.length == Argument.Mastodon.values().length) {
42
      twitterConfiguration = null;
43
      mastodonClient =
44
          createMastodonClient(
45
              args[Argument.Mastodon.INSTANCE_NAME.ordinal()],
46
              args[Argument.Mastodon.ACCESS_TOKEN.ordinal()]);
47
    } else {
48
      throw createIllegalArgumentException();
49
    }
50
  }
51

  
52
  private static Configuration createTwitterConfiguration(
53
      String twitterConsumerKey,
54
      String twitterConsumerSecret,
55
      String twitterAccessToken,
56
      String twitterAccessTokenSecret) {
57
    return new ConfigurationBuilder()
58
        .setOAuthConsumerKey(twitterConsumerKey)
59
        .setOAuthConsumerSecret(twitterConsumerSecret)
60
        .setOAuthAccessToken(twitterAccessToken)
61
        .setOAuthAccessTokenSecret(twitterAccessTokenSecret)
62
        .build();
63
  }
64

  
65
  private static MastodonClient createMastodonClient(
66
      String mastodonInstanceName, String mastodonAccessToken) {
67
    return new MastodonClient.Builder(mastodonInstanceName, new OkHttpClient.Builder(), new Gson())
68
        .accessToken(mastodonAccessToken)
69
        .useStreamingApi()
70
        .build();
71
  }
72

  
73
  private static IllegalArgumentException createIllegalArgumentException() {
74
    return new IllegalArgumentException(
75
        Argument.createExceptionMessage()
76
            + "\n"
77
            + Argument.Twitter.createExceptionMessage()
78
            + "\n"
79
            + Argument.Mastodon.createExceptionMessage());
80
  }
81

  
82
  /**
83
   * Java アプリケーション起動時に指定する引数の定義
84
   *
85
   * @author みぞ@CrazyBeatCoder
86
   */
87
  private enum Argument {
88
    TWITTER_CONSUMER_KEY, //
89
    TWITTER_CONSUMER_SECRET, //
90
    TWITTER_ACCESS_TOKEN, //
91
    TWITTER_ACCESS_TOKEN_SECRET, //
92
    MASTODON_INSTANCE_NAME, //
93
    MASTODON_ACCESS_TOKEN, //
94
    ;
95

  
96
    @NotNull
97
    private static String createExceptionMessage() {
98
      StringBuilder exceptionMessage =
99
          new StringBuilder("Twitter と Mastodon の両方を読み上げる場合、 ")
100
              .append(values().length)
101
              .append(" つの引数を指定してください。\n");
102
      for (Argument arg : values()) {
103
        exceptionMessage
104
            .append(arg.ordinal() + 1)
105
            .append(" つ目: ")
106
            .append(arg.getDetail())
107
            .append("\n");
108
      }
109

  
110
      return exceptionMessage.toString();
111
    }
112

  
113
    @NotNull
114
    private String getDetail() throws IllegalStateException {
115
      switch (this) {
116
        case TWITTER_CONSUMER_KEY:
117
          return "Twitter Application's Consumer Key (API Key)";
118
        case TWITTER_CONSUMER_SECRET:
119
          return "Twitter Application's Consumer Secret (API Secret)";
120
        case TWITTER_ACCESS_TOKEN:
121
          return "Twitter Account's Access Token";
122
        case TWITTER_ACCESS_TOKEN_SECRET:
123
          return "Twitter Account's Access Token Secret";
124
        case MASTODON_INSTANCE_NAME:
125
          return "Mastodon Instance Name";
126
        case MASTODON_ACCESS_TOKEN:
127
          return "Mastodon Account's Access Token";
128
        default:
129
          throw new IllegalStateException("getDetail this: " + this);
130
      }
131
    }
132

  
133
    private enum Twitter {
134
      CONSUMER_KEY, //
135
      CONSUMER_SECRET, //
136
      ACCESS_TOKEN, //
137
      ACCESS_TOKEN_SECRET, //
138
      ;
139

  
140
      @NotNull
141
      private static String createExceptionMessage() {
142
        StringBuilder exceptionMessage =
143
            new StringBuilder("Twitter のみを読み上げる場合、 ")
144
                .append(values().length)
145
                .append(" つの引数を指定してください。\n");
146
        for (Argument.Twitter arg : values()) {
147
          exceptionMessage
148
              .append(arg.ordinal() + 1)
149
              .append(" つ目: ")
150
              .append(arg.getDetail())
151
              .append("\n");
152
        }
153
        return exceptionMessage.toString();
154
      }
155

  
156
      @NotNull
157
      private String getDetail() throws IllegalStateException {
158
        switch (this) {
159
          case CONSUMER_KEY:
160
            return TWITTER_CONSUMER_KEY.getDetail();
161
          case CONSUMER_SECRET:
162
            return TWITTER_CONSUMER_SECRET.getDetail();
163
          case ACCESS_TOKEN:
164
            return TWITTER_ACCESS_TOKEN.getDetail();
165
          case ACCESS_TOKEN_SECRET:
166
            return TWITTER_ACCESS_TOKEN_SECRET.getDetail();
167
          default:
168
            throw new IllegalStateException("getDetail this: " + this);
169
        }
170
      }
171
    }
172

  
173
    private enum Mastodon {
174
      INSTANCE_NAME, //
175
      ACCESS_TOKEN, //
176
      ;
177

  
178
      @NotNull
179
      private static String createExceptionMessage() {
180
        StringBuilder exceptionMessage =
181
            new StringBuilder("Mastodon のみを読み上げる場合、 ")
182
                .append(values().length)
183
                .append(" つの引数を指定してください。\n");
184
        for (Argument.Mastodon arg : values()) {
185
          exceptionMessage
186
              .append(arg.ordinal() + 1)
187
              .append(" つ目: ")
188
              .append(arg.getDetail())
189
              .append("\n");
190
        }
191
        return exceptionMessage.toString();
192
      }
193

  
194
      @NotNull
195
      private String getDetail() throws IllegalStateException {
196
        switch (this) {
197
          case INSTANCE_NAME:
198
            return MASTODON_INSTANCE_NAME.getDetail();
199
          case ACCESS_TOKEN:
200
            return MASTODON_ACCESS_TOKEN.getDetail();
201
          default:
202
            throw new IllegalStateException("getDetail this: " + this);
203
        }
204
      }
205
    }
206
  }
207
}
src/com/mizo0203/timeline/talker/Main.java
1
package com.mizo0203.timeline.talker;
2

  
3
import org.jetbrains.annotations.NotNull;
4

  
5
/**
6
 * Java アプリケーション起動時に実行されるクラス
7
 *
8
 * @author みぞ@CrazyBeatCoder
9
 */
10
public class Main {
11

  
12
  public static void main(@NotNull String[] args) {
13
    TimelineTalker twitterTimelineTalker;
14
    TimelineTalker mastodonTimelineTalker;
15
    Talker talker;
16

  
17
    try {
18
      Arguments arguments = new Arguments(args);
19
      talker = new Talker();
20

  
21
      if (arguments.twitterConfiguration != null) {
22
        twitterTimelineTalker = new TwitterTimelineTalker(arguments.twitterConfiguration, talker);
23
        twitterTimelineTalker.start();
24
      }
25

  
26
      if (arguments.mastodonClient != null) {
27
        mastodonTimelineTalker = new MastodonTimelineTalker(arguments.mastodonClient, talker);
28
        mastodonTimelineTalker.start();
29
      }
30

  
31
    } catch (@NotNull IllegalArgumentException | IllegalStateException e) {
32
      System.err.println(e.getMessage());
33
      return;
34
    }
35

  
36
    talker.talkAlternatelyAsync("アプリケーションを起動しました");
37
  }
38
}
src/com/mizo0203/timeline/talker/MastodonTimelineTalker.java
1
package com.mizo0203.timeline.talker;
2

  
3
import com.mizo0203.timeline.talker.util.DisplayNameUtil;
4
import com.mizo0203.timeline.talker.util.HTMLParser;
5
import com.mizo0203.timeline.talker.util.UrlUtil;
6
import com.sys1yagi.mastodon4j.MastodonClient;
7
import com.sys1yagi.mastodon4j.api.Handler;
8
import com.sys1yagi.mastodon4j.api.entity.Account;
9
import com.sys1yagi.mastodon4j.api.entity.Notification;
10
import com.sys1yagi.mastodon4j.api.entity.Status;
11
import com.sys1yagi.mastodon4j.api.method.Streaming;
12
import org.jetbrains.annotations.NotNull;
13

  
14
import java.io.IOException;
15
import java.nio.charset.StandardCharsets;
16

  
17
public class MastodonTimelineTalker implements TimelineTalker {
18

  
19
  @NotNull private final Streaming mStreaming;
20
  @NotNull private final OnStatusEvent mOnStatusEvent;
21

  
22
  /* package */ MastodonTimelineTalker(MastodonClient client, Talker talker) {
23
    mStreaming = new Streaming(client);
24
    mOnStatusEvent = new OnStatusEvent(talker);
25
  }
26

  
27
  @Override
28
  public void start() {
29

  
30
    try {
31
      mStreaming.user(mOnStatusEvent);
32
    } catch (Exception e) {
33
      e.printStackTrace();
34
    }
35
  }
36

  
37
  private static class OnStatusEvent implements Handler {
38

  
39
    private final Talker mTalker;
40

  
41
    private OnStatusEvent(Talker talker) {
42
      mTalker = talker;
43
    }
44

  
45
    @Override
46
    public void onStatus(@NotNull Status status) {
47

  
48
      try {
49
        final StringBuffer buffer = new StringBuffer();
50
        final Status reblogStatus = status.getReblog();
51

  
52
        String displayName = "誰か";
53
        if (status.getAccount() != null) {
54
          displayName = status.getAccount().getDisplayName();
55
        }
56
        if (reblogStatus != null) {
57
          String reblogDisplayName = "誰か";
58
          if (status.getAccount() != null) {
59
            displayName = status.getAccount().getDisplayName();
60
          }
61
          buffer.append(DisplayNameUtil.removeContext(displayName)).append("さんがブースト。");
62
          buffer.append(DisplayNameUtil.removeContext(reblogDisplayName)).append("さんから、");
63
          buffer.append(
64
              new HTMLParser().parse(reblogStatus.getContent(), StandardCharsets.UTF_8, true));
65
        } else {
66
          buffer.append(DisplayNameUtil.removeContext(displayName)).append("さんから、");
67
          buffer.append(new HTMLParser().parse(status.getContent(), StandardCharsets.UTF_8, true));
68
        }
69

  
70
        final String talkText = UrlUtil.convURLEmpty(buffer).replaceAll("\n", "。");
71
        mTalker.talkAlternatelyAsync(talkText);
72
      } catch (IOException e) {
73
        e.printStackTrace();
74
      }
75
    }
76

  
77
    @Override
78
    public void onNotification(@NotNull Notification notification) {
79
      final StringBuilder buffer = new StringBuilder();
80
      final Account account = notification.getAccount();
81

  
82
      if (account == null) {
83
        return;
84
      }
85

  
86
      switch (notification.getType()) {
87
        case "mention":
88
          buffer
89
              .append(DisplayNameUtil.removeContext(account.getDisplayName()))
90
              .append("さんがあなたをメンションしました。");
91
          break;
92
        case "reblog":
93
          buffer
94
              .append(DisplayNameUtil.removeContext(account.getDisplayName()))
95
              .append("さんがあなたのトゥートをブーストしました。");
96
          break;
97
        case "favourite":
98
          buffer
99
              .append(DisplayNameUtil.removeContext(account.getDisplayName()))
100
              .append("さんがあなたのトゥートをお気に入りに登録しました。");
101
          break;
102
        case "follow":
103
          buffer
104
              .append(DisplayNameUtil.removeContext(account.getDisplayName()))
105
              .append("さんにフォローされました。");
106
          break;
107
        default:
108
          return;
109
      }
110

  
111
      final String talkText = buffer.toString();
112
      mTalker.talkAlternatelyAsync(talkText);
113
    }
114

  
115
    @Override
116
    public void onDelete(long id) {
117
      /* no op */
118
    }
119
  }
120
}
src/com/mizo0203/timeline/talker/RuntimeUtil.java
1
package com.mizo0203.timeline.talker;
2

  
3
import java.io.IOException;
4

  
5
public class RuntimeUtil {
6

  
7
  public static void execute(String[] cmdarray) {
8
    try {
9
      Process process = Runtime.getRuntime().exec(cmdarray);
10
      process.waitFor();
11
      process.destroy();
12
    } catch (IOException | InterruptedException e) {
13
      e.printStackTrace();
14
    }
15
  }
16

  
17
}
src/com/mizo0203/timeline/talker/Talker.java
1 1
package com.mizo0203.timeline.talker;
2 2

  
3
import com.mizo0203.timeline.talker.util.RuntimeUtil;
4
import org.jetbrains.annotations.NotNull;
5

  
3 6
import java.io.*;
4 7
import java.util.concurrent.ExecutorService;
5 8
import java.util.concurrent.Executors;
......
10 13

  
11 14
  private final ExecutorService mSingleThreadExecutor = Executors.newSingleThreadExecutor();
12 15

  
16
  @NotNull private YukkuriVoice mNextVoice = Talker.YukkuriVoice.REIMU;
17

  
13 18
  public Talker() throws IllegalStateException, SecurityException {
14 19
    File file = new File(AQUESTALK_PI_PATH);
15 20
    if (!file.isFile()) {
16
      throw new IllegalStateException(file.getPath() + " に AquesTalk Pi がありません。\n"
17
          + "https://www.a-quest.com/products/aquestalkpi.html\n" + "からダウンロードしてください。");
21
      throw new IllegalStateException(
22
          file.getPath()
23
              + " に AquesTalk Pi がありません。\n"
24
              + "https://www.a-quest.com/products/aquestalkpi.html\n"
25
              + "からダウンロードしてください。");
18 26
    }
19 27
    if (!file.canExecute()) {
20 28
      throw new IllegalStateException(file.getPath() + " に実行権限がありません。");
21 29
    }
22 30
  }
23 31

  
24
  public void talkAsync(final String text, final YukkuriVoice voice) {
25
    mSingleThreadExecutor.submit(new Runnable() {
26

  
27
      @Override
28
      public void run() {
29
        try {
30
          File file = new File("text.txt");
31
          PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
32
          pw.println(text);
33
          pw.flush();
34
          pw.close();
35
          RuntimeUtil.execute(new String[] {AQUESTALK_PI_PATH, "-v", voice.value, "-f", "text.txt",
36
              "-o", "out.wav"});
37
          RuntimeUtil.execute(new String[] {"sh", "-c", "aplay < out.wav"}); // 起動コマンドを指定する
38
          Thread.sleep(2000);
39
        } catch (IOException | InterruptedException e) {
40
          e.printStackTrace();
41
        }
42
      }
43

  
44
    });
32
  public void talkAlternatelyAsync(final String text) {
33
    mSingleThreadExecutor.submit(
34
        new Runnable() {
35

  
36
          @Override
37
          public void run() {
38
            try {
39
              File file = new File("text.txt");
40
              PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
41
              pw.println(text);
42
              pw.flush();
43
              pw.close();
44
              RuntimeUtil.execute(
45
                  new String[] {
46
                    AQUESTALK_PI_PATH, "-v", mNextVoice.value, "-f", "text.txt", "-o", "out.wav"
47
                  });
48
              RuntimeUtil.execute(new String[] {"sh", "-c", "aplay < out.wav"}); // 起動コマンドを指定する
49

  
50
              // 読み上げは、霊夢と魔理沙が交互に行なう
51
              if (mNextVoice == Talker.YukkuriVoice.REIMU) {
52
                mNextVoice = Talker.YukkuriVoice.MARISA;
53
              } else {
54
                mNextVoice = Talker.YukkuriVoice.REIMU;
55
              }
56

  
57
              Thread.sleep(2000);
58
            } catch (@NotNull IOException | InterruptedException e) {
59
              e.printStackTrace();
60
            }
61
          }
62
        });
45 63
  }
46 64

  
47
  public static enum YukkuriVoice {
65
  public enum YukkuriVoice {
48 66

  
49
    /**
50
     * ゆっくりボイス - 霊夢
51
     */
67
    /** ゆっくりボイス - 霊夢 */
52 68
    REIMU("f1"), //
53 69

  
54
    /**
55
     * ゆっくりボイス - 魔理沙
56
     */
70
    /** ゆっくりボイス - 魔理沙 */
57 71
    MARISA("f2"), //
58 72
    ;
59 73

  
60 74
    private final String value;
61 75

  
62
    private YukkuriVoice(String value) {
76
    YukkuriVoice(String value) {
63 77
      this.value = value;
64 78
    }
65 79
  }
66

  
67 80
}
src/com/mizo0203/timeline/talker/TimelineTalker.java
1 1
package com.mizo0203.timeline.talker;
2 2

  
3
import twitter4j.*;
4
import twitter4j.conf.Configuration;
3
/* package */ interface TimelineTalker {
5 4

  
6
import java.util.Collections;
7
import java.util.Locale;
8
import java.util.Timer;
9
import java.util.TimerTask;
10
import java.util.concurrent.TimeUnit;
11
import java.util.regex.Matcher;
12
import java.util.regex.Pattern;
13

  
14
public class TimelineTalker {
15

  
16
  /**
17
   * ISO 639 言語コード - 日本語 (ja)
18
   */
19
  public static final String LANG_JA = Locale.JAPAN.getLanguage();
20

  
21
  private final RequestHomeTimelineTimerTask mRequestHomeTimelineTimerTask;
22

  
23
  public TimelineTalker(Configuration configuration, Talker talker) {
24
    Twitter twitter = new TwitterFactory(configuration).getInstance();
25
    mRequestHomeTimelineTimerTask = new RequestHomeTimelineTimerTask(twitter, talker);
26
  }
27

  
28
  private static String getUserNameWithoutContext(String name) {
29
    Pattern p = Pattern.compile("([^@@]+).+");
30
    Matcher m = p.matcher(name);
31
    return m.replaceFirst("$1");
32
  }
33

  
34
  public void start() {
35
    new Timer().schedule(mRequestHomeTimelineTimerTask, 0L, TimeUnit.MINUTES.toMillis(1));
36
  }
37

  
38
  private static class RequestHomeTimelineTimerTask extends TimerTask {
39

  
40
    private static final int HOME_TIMELINE_COUNT_MAX = 200;
41
    private static final int HOME_TIMELINE_COUNT_MIN = 1;
42

  
43
    private final Twitter mTwitter;
44
    private final Talker mTalker;
45

  
46
    private Talker.YukkuriVoice mYukkuriVoice = Talker.YukkuriVoice.REIMU;
47

  
48
    /**
49
     * mStatusSinceId より大きい(つまり、より新しい) ID を持つ HomeTimeline をリクエストする
50
     */
51
    private long mStatusSinceId = 1L;
52

  
53
    private boolean mIsUpdatedStatusSinceId = false;
54

  
55
    private RequestHomeTimelineTimerTask(Twitter twitter, Talker talker) {
56
      mTwitter = twitter;
57
      mTalker = talker;
58
    }
59

  
60
    /**
61
     * The action to be performed by this timer task.
62
     */
63
    @Override
64
    public void run() {
65
      try {
66
        // mStatusSinceId が未更新ならば、 Status を 1 つだけ取得する
67
        int count = mIsUpdatedStatusSinceId ? HOME_TIMELINE_COUNT_MAX : HOME_TIMELINE_COUNT_MIN;
68
        Paging paging = new Paging(1, count, mStatusSinceId);
69
        ResponseList<Status> statusResponseList = mTwitter.getHomeTimeline(paging);
70

  
71
        if (statusResponseList.isEmpty()) {
72
          return;
73
        }
74

  
75
        // mStatusSinceId を、取得した最新の ID に更新する
76
        mStatusSinceId = statusResponseList.get(0).getId();
77
        mIsUpdatedStatusSinceId = true;
78

  
79
        // Status が古い順になるよう、 statusResponseList を逆順に並び替える
80
        Collections.reverse(statusResponseList);
81

  
82
        for (Status status : statusResponseList) {
83
          onStatus(status);
84
        }
85

  
86
      } catch (TwitterException e) {
87
        e.printStackTrace();
88
      }
89
    }
90

  
91
    private void onStatus(final Status status) {
92
      if (!LANG_JA.equalsIgnoreCase(status.getLang())) {
93
        return;
94
      }
95

  
96
      final StringBuffer buffer = new StringBuffer();
97

  
98
      if (status.isRetweet()) {
99
        Status retweetedStatus = status.getRetweetedStatus();
100
        buffer.append(getUserNameWithoutContext(status.getUser().getName()) + "さんがリツイート。");
101
        buffer.append(getUserNameWithoutContext(retweetedStatus.getUser().getName()) + "さんから、");
102
        buffer.append(retweetedStatus.getText());
103
      } else {
104
        buffer.append(getUserNameWithoutContext(status.getUser().getName()) + "さんから、");
105
        buffer.append(status.getText());
106
      }
107

  
108
      mTalker.talkAsync(UrlUtil.convURLEmpty(buffer).replaceAll("\n", "。"), mYukkuriVoice);
109

  
110
      // 読み上げは、霊夢と魔理沙が交互に行なう
111
      if (mYukkuriVoice == Talker.YukkuriVoice.REIMU) {
112
        mYukkuriVoice = Talker.YukkuriVoice.MARISA;
113
      } else {
114
        mYukkuriVoice = Talker.YukkuriVoice.REIMU;
115
      }
116
    }
117
  }
5
  /* package */ void start();
118 6
}
src/com/mizo0203/timeline/talker/TwitterTimelineTalker.java
1
package com.mizo0203.timeline.talker;
2

  
3
import com.mizo0203.timeline.talker.util.DisplayNameUtil;
4
import com.mizo0203.timeline.talker.util.UrlUtil;
5
import org.jetbrains.annotations.NotNull;
6
import twitter4j.*;
7
import twitter4j.conf.Configuration;
8

  
9
import java.util.Collections;
10
import java.util.Locale;
11
import java.util.Timer;
12
import java.util.TimerTask;
13
import java.util.concurrent.TimeUnit;
14

  
15
public class TwitterTimelineTalker implements TimelineTalker {
16

  
17
  /** ISO 639 言語コード - 日本語 (ja) */
18
  private static final String LANG_JA = Locale.JAPAN.getLanguage();
19

  
20
  @NotNull private final RequestHomeTimelineTimerTask mRequestHomeTimelineTimerTask;
21

  
22
  public TwitterTimelineTalker(@NotNull Configuration configuration, Talker talker) {
23
    Twitter twitter = new TwitterFactory(configuration).getInstance();
24
    mRequestHomeTimelineTimerTask = new RequestHomeTimelineTimerTask(twitter, talker);
25
  }
26

  
27
  @Override
28
  public void start() {
29
    new Timer().schedule(mRequestHomeTimelineTimerTask, 0L, TimeUnit.MINUTES.toMillis(1));
30
  }
31

  
32
  private static class RequestHomeTimelineTimerTask extends TimerTask {
33

  
34
    private static final int HOME_TIMELINE_COUNT_MAX = 200;
35
    private static final int HOME_TIMELINE_COUNT_MIN = 1;
36

  
37
    private final Twitter mTwitter;
38
    private final Talker mTalker;
39

  
40
    /** mStatusSinceId より大きい(つまり、より新しい) ID を持つ HomeTimeline をリクエストする */
41
    private long mStatusSinceId = 1L;
42

  
43
    private boolean mIsUpdatedStatusSinceId = false;
44

  
45
    private RequestHomeTimelineTimerTask(Twitter twitter, Talker talker) {
46
      mTwitter = twitter;
47
      mTalker = talker;
48
    }
49

  
50
    /** The action to be performed by this timer task. */
51
    @Override
52
    public void run() {
53
      try {
54
        // mStatusSinceId が未更新ならば、 Status を 1 つだけ取得する
55
        int count = mIsUpdatedStatusSinceId ? HOME_TIMELINE_COUNT_MAX : HOME_TIMELINE_COUNT_MIN;
56
        Paging paging = new Paging(1, count, mStatusSinceId);
57
        ResponseList<Status> statusResponseList = mTwitter.getHomeTimeline(paging);
58

  
59
        if (statusResponseList.isEmpty()) {
60
          return;
61
        }
62

  
63
        // mStatusSinceId を、取得した最新の ID に更新する
64
        mStatusSinceId = statusResponseList.get(0).getId();
65
        mIsUpdatedStatusSinceId = true;
66

  
67
        // Status が古い順になるよう、 statusResponseList を逆順に並び替える
68
        Collections.reverse(statusResponseList);
69

  
70
        for (Status status : statusResponseList) {
71
          onStatus(status);
72
        }
73

  
74
      } catch (TwitterException e) {
75
        e.printStackTrace();
76
      }
77
    }
78

  
79
    private void onStatus(final Status status) {
80
      if (!LANG_JA.equalsIgnoreCase(status.getLang())) {
81
        return;
82
      }
83

  
84
      final StringBuffer buffer = new StringBuffer();
85

  
86
      if (status.isRetweet()) {
87
        Status retweetedStatus = status.getRetweetedStatus();
88
        buffer
89
            .append(DisplayNameUtil.removeContext(status.getUser().getName()))
90
            .append("さんがリツイート。");
91
        buffer
92
            .append(DisplayNameUtil.removeContext(retweetedStatus.getUser().getName()))
93
            .append("さんから、");
94
        buffer.append(retweetedStatus.getText());
95
      } else {
96
        buffer.append(DisplayNameUtil.removeContext(status.getUser().getName())).append("さんから、");
97
        buffer.append(status.getText());
98
      }
99

  
100
      mTalker.talkAlternatelyAsync(UrlUtil.convURLEmpty(buffer).replaceAll("\n", "。"));
101
    }
102
  }
103
}
src/com/mizo0203/timeline/talker/UrlUtil.java
1
package com.mizo0203.timeline.talker;
2

  
3
import java.util.regex.Matcher;
4
import java.util.regex.Pattern;
5

  
6
/**
7
 * http://chat-messenger.net/blog-entry-40.html
8
 */
9
public class UrlUtil {
10
  /** URLを抽出するための正規表現パターン */
11
  private static final Pattern convURLLinkPtn = Pattern.compile(
12
      "(http://|https://){1}[\\w\\.\\-/:\\#\\?\\=\\&\\;\\%\\~\\+]+", Pattern.CASE_INSENSITIVE);
13

  
14
  /**
15
   * 指定された文字列内のURLを、正規表現を使用し、 空文字列に変換する。
16
   * 
17
   * @param str 指定の文字列。
18
   * @return リンクに変換された文字列。
19
   */
20
  public static String convURLEmpty(CharSequence str) {
21
    Matcher matcher = convURLLinkPtn.matcher(str);
22
    return matcher.replaceAll("");
23
  }
24
}
src/com/mizo0203/timeline/talker/util/DisplayNameUtil.java
1
package com.mizo0203.timeline.talker.util;
2

  
3
import org.jetbrains.annotations.NotNull;
4

  
5
import java.util.regex.Matcher;
6
import java.util.regex.Pattern;
7

  
8
public class DisplayNameUtil {
9

  
10
  public static String removeContext(@NotNull String name) {
11
    Pattern p = Pattern.compile("([^@@]+).+");
12
    Matcher m = p.matcher(name);
13
    return m.replaceFirst("$1");
14
  }
15
}
src/com/mizo0203/timeline/talker/util/HTMLParser.java
1
package com.mizo0203.timeline.talker.util;
2

  
3
import org.apache.commons.io.IOUtils;
4
import org.jetbrains.annotations.NotNull;
5

  
6
import javax.swing.text.html.HTMLEditorKit;
7
import javax.swing.text.html.parser.ParserDelegator;
8
import java.io.IOException;
9
import java.io.InputStreamReader;
10
import java.nio.charset.Charset;
11

  
12
public class HTMLParser {
13

  
14
  @NotNull
15
  public String parse(@NotNull String html, @NotNull Charset encoding, boolean ignoreCharSet)
16
      throws IOException {
17
    try (InputStreamReader r =
18
        new InputStreamReader(IOUtils.toInputStream(html, encoding), encoding)) {
19
      HTMLParserCallback hp = new HTMLParserCallback();
20
      ParserDelegator parser = new ParserDelegator();
21
      parser.parse(r, hp, ignoreCharSet);
22
      return hp.getText();
23
    }
24
  }
25

  
26
  /**
27
   * http://www.my-notebook.net/736a69e0-820c-423b-9047-a02b8a9eefb1.html
28
   *
29
   * <p>HTMLParser.java
30
   */
31
  private static class HTMLParserCallback extends HTMLEditorKit.ParserCallback {
32
    private final StringBuffer sb = new StringBuffer();
33

  
34
    private String getText() {
35
      return sb.toString();
36
    }
37

  
38
    @Override
39
    public void handleText(@NotNull char[] data, int pos) {
40
      sb.append(new String(data));
41
      sb.append(System.getProperty("line.separator"));
42
    }
43
  }
44
}
src/com/mizo0203/timeline/talker/util/RuntimeUtil.java
1
package com.mizo0203.timeline.talker.util;
2

  
3
import org.jetbrains.annotations.NotNull;
4

  
5
import java.io.IOException;
6

  
7
public class RuntimeUtil {
8

  
9
  public static void execute(String[] cmdarray) {
10
    try {
11
      Process process = Runtime.getRuntime().exec(cmdarray);
12
      process.waitFor();
13
      process.destroy();
14
    } catch (@NotNull IOException | InterruptedException e) {
15
      e.printStackTrace();
16
    }
17
  }
18
}
src/com/mizo0203/timeline/talker/util/UrlUtil.java
1
package com.mizo0203.timeline.talker.util;
2

  
3
import org.jetbrains.annotations.NotNull;
4

  
5
import java.util.regex.Matcher;
6
import java.util.regex.Pattern;
7

  
8
/** http://chat-messenger.net/blog-entry-40.html */
9
public class UrlUtil {
10
  /** URLを抽出するための正規表現パターン */
11
  @SuppressWarnings("Annotator")
12
  private static final Pattern convURLLinkPtn =
13
      Pattern.compile(
14
          "(http://|https://){1}[\\w\\.\\-/:\\#\\?\\=\\&\\;\\%\\~\\+]+", Pattern.CASE_INSENSITIVE);
15

  
16
  /**
17
   * 指定された文字列内のURLを、正規表現を使用し、 空文字列に変換する。
18
   *
19
   * @param str 指定の文字列。
20
   * @return リンクに変換された文字列。
21
   */
22
  public static String convURLEmpty(@NotNull CharSequence str) {
23
    Matcher matcher = convURLLinkPtn.matcher(str);
24
    return matcher.replaceAll("");
25
  }
26
}

他の形式にエクスポート: Unified diff